-
Notifications
You must be signed in to change notification settings - Fork 86
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add Alpha Support for Server Side Apply (2nd attempt) #246
Conversation
internal/controller/object/object.go
Outdated
// the apiserver, so that we can compare it with the extracted state to | ||
// decide whether the object is up-to-date or not. | ||
desiredObj := desired.DeepCopy() | ||
if err := c.client.Patch(ctx, desiredObj, client.Apply, client.ForceOwnership, client.FieldOwner(ssaFieldOwner(cr.Name)), client.DryRunAll); err != nil { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This a workaround for kubernetes/kubernetes#115563
b6ab42d
to
2e2b035
Compare
@@ -123,8 +123,10 @@ func enqueueObjectsForReferences(ca cache.Cache, log logging.Logger) func(ctx co | |||
} | |||
// queue those Objects for reconciliation | |||
for _, o := range objects.Items { | |||
log.Info("Enqueueing Object because referenced resource changed", "name", o.GetName(), "referencedGVK", rGVK.String(), "referencedName", ev.Object.GetName(), "providerConfig", pc) | |||
q.Add(reconcile.Request{NamespacedName: types.NamespacedName{Name: o.GetName()}}) | |||
if o.Spec.Watch { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why is this necessary?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We only want to enqueue the Object if spec.watch
is set to true. Not every Object wants to watch the referenced resources. If you're questioning why we have such a field in the first place, I have a comment for that here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not questioning the watch field. But why we make the q.Add
conditional on it. I understand that we don't start watches on their referenced resources, but here we are not doing that, but queuing them.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, I see.
Watches started based on GVK not including their namespaced name, so, another object with spec.watch: true
might have started a watch on that given GVK. For example, think two Objects; one referencing secret: foo
with watch: true
, another one referencing secret: bar
with watch: false
. Even though the latter does not start watch on Secrets, it was started with the former. So, we need to filter out here, while adding to the queue otherwise the latter object will be getting watch events even not desired.
return managed.ExternalObservation{}, errors.Wrap(err, "cannot extract SSA") | ||
} | ||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This change let's me question the approach. Why do we have to compare the observation with our desired state? Why don't we watch the object, and re-apply our desired state on every event? If we have a apiserver roundtrip anyway for dry-run, then we can also just directly server-side-apply.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
re-apply our desired state on every event
That wouldn't work since, in some cases, re-applying the same manifest simply bumps the resourceVersion and causing another update event. So, if we implement it in that way, we would end up in an update loop, i.e., apply -> update event -> apply -> update event ...
There are various issues that I have a hunch that all boils down to the same root cause:
- Server-side apply bumps resourceVersion when re-applying the same object kubernetes/kubernetes#124605
- Server side apply increments resource version for certain fields every second kubernetes/kubernetes#121404
- Server-side apply has trouble with empty map kubernetes/kubernetes#124050
applyconfiguration.Extract
has poor interaction with defaulting kubernetes/kubernetes#115563
If we have a apiserver roundtrip anyway for dry-run
So, if the above bug didn't exist, most probably we wouldn't need that dry-run as well. This is already a workaround for that. Once it is fixed (and prevalent enough), we can remove this workaround hence get rid of that dry run call.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So, if we implement it in that way, we would end up in an update loop, i.e., apply -> update event -> apply -> update event
Given that composition functions uses SSA to apply composed resources, should we expect the same problem in c/c if realtime compositions are used together with functions?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Probably.
I reproduced with kubectl using the deployment in this issue, and know that an empty map triggers it. So, it doesn't happen for all resources but for some. I am almost sure there will be some MRs resulting in similar behavior under certain conditions. This is reproducing with a CRD / CR for example.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's quite unfortunate. 🙁 I've added some notes to the SSA and realtime composition lifecycle tracking issues just in case we see this in the wild.
3f3a602
to
c6d5a72
Compare
b2d72c5
to
db2a37c
Compare
@bobh66 just pushed a commit wrapping the work up with best effort. I believe the PR is ready to go. @sttts opened a follow up issue to avoid the dry-run calls we discussed above. We will make sure to resolve it before promoting the feature to beta (and/or shipping v1.0.0) |
a1a9349
to
db2be77
Compare
@@ -66,6 +66,7 @@ func main() { | |||
|
|||
enableManagementPolicies = app.Flag("enable-management-policies", "Enable support for Management Policies.").Default("true").Envar("ENABLE_MANAGEMENT_POLICIES").Bool() | |||
enableWatches = app.Flag("enable-watches", "Enable support for watching resources.").Default("false").Envar("ENABLE_WATCHES").Bool() | |||
enableServerSideApply = app.Flag("enable-server-side-apply", "Enable server side apply to sync object manifests to k8s API.").Default("false").Envar("ENABLE_SERVER_SIDE_APPLY").Bool() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this meant as a feature gate? Doesn't look like from the name. I don't like to force ppl to understand so low-level concepts, i.e. this kind of flag should not exist medium-term.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What is wrong with name, what could be an alternative?
I agree that people shouldn't need to understand details on how we sync the desired state, but don't feel comfortable to replace existing mechanism in place, so wanted to put behind a feature flag. I think similar to https://github.com/crossplane/crossplane/blob/2a427fc89da4a2e86f75eaecfa8328f53e19f33b/cmd/crossplane/core/core.go#L115
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If this is a feature gate that goes away after alpha/beta, then this is fine. Wasn't sure whether it is meant to stay forever as a knob.
} | ||
|
||
if c.ssaEnabled { | ||
dc, err := discovery.NewDiscoveryClientForConfig(rc) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This looks wrong. It is called on every reconcile, isn't it? Instantiating a new discovery client is heavy.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The problem is, this is not something static, we need to initialize discovery client for the kubeconfig configured in the ProviderConfig referenced in the Object. It can change anytime, so, we need to initialize during reconcile unless we do some sort of caching which doesn't feel like trivial.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We have been digging a bit deeper. The extractor below downloads the OpenAPI v2 schema via dc
. This is a big (multi-megabyte) proto file. It will grow even more (a lot) if you have many CRDs from providers installed. And the reconciler will do that on every reconcile, i.e. tens-of-megabyte download and unmarshalling. This is likely a blocker for the approach 🔴.
The solution might be caching of the discovery client by provider config.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
turkenh#3 is merged into this feature branch, that implements a custom CachingUnstructuredExtractor, which utilizes OpenAPI v3 discovery and caching per GV.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The original blocker problem is solved with the caching.
@turkenh is there a way to move this forward? |
@bobh66 we are on it. |
7fdc130
to
f3c2080
Compare
) | ||
|
||
// cachingUnstructuredExtractor is a caching implementation of v1.UnstructuredExtractor | ||
// using OpenAPI V3 discovery information. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
be clear what it caches.
func (cm *GVKParserCacheManager) LoadOrNewCacheForProviderConfig(pc *v1alpha1.ProviderConfig) (*GVKParserCache, error) { | ||
cm.mu.Lock() | ||
defer cm.mu.Unlock() | ||
sc, ok := cm.cacheStore[pc.GetUID()] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why is the uid enough? Couldn't the content change?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
cache manager stores the collection of caches per provider config.
The actual cache is the one that is returned, this is done at Connect time and given to the reconciliation scope.
Then the content changes are handled there.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
At connect time you call LoadOrNewCacheForProviderConfig
, but it will return the same object on every reconcile. If the ProviderConfig is changes, the UID will still be the same and with that the cache is potentially invalid. What do I miss?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
At connect time you call LoadOrNewCacheForProviderConfig, but it will return the same object on every reconcile.
yes, we get the same GvkParserCache here for the PC. LoadOrNewCacheForProviderConfig
just gives you the actual cache to maintain. We can think of GvkParserCacheManager
like a mere storage organizer of caches, it just points you to the right drawer reserved for PC. Then we pass the GvkParserCache we got to the extractor at Connect time.
The cache itself is maintained after that point by the extractor here
at each extract request:
A v3 discovery is initiated that returns a collection of all served gv paths to Etagged API paths, then according to the output :
- stale entries are invalidated (non-matching Etags, APIs not being served anymore)
- then we retrieve the schema (if it is not on the cache after invalidations) for the relevant GV of object that is being extracted, and cache it with its ETag.
So, in the case of the ProviderConfig change, the returned discovery information will change, as the discovery client itself is rebuilt at each reconcile. Then, the invalidations will take place, so we operate on up-to-date info.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ack. Seems sounds. This is crucial information that needs to be in some comments
… config Signed-off-by: Erhan Cagirici <[email protected]>
Signed-off-by: Erhan Cagirici <[email protected]>
Signed-off-by: Erhan Cagirici <[email protected]>
Signed-off-by: Erhan Cagirici <[email protected]>
Signed-off-by: Erhan Cagirici <[email protected]>
Signed-off-by: Erhan Cagirici <[email protected]>
Signed-off-by: Erhan Cagirici <[email protected]>
Signed-off-by: Erhan Cagirici <[email protected]>
Signed-off-by: Erhan Cagirici <[email protected]>
Signed-off-by: Erhan Cagirici <[email protected]>
Signed-off-by: Erhan Cagirici <[email protected]>
Signed-off-by: Erhan Cagirici <[email protected]>
Signed-off-by: Erhan Cagirici <[email protected]>
This reverts commit 3ea9c0b. Signed-off-by: Erhan Cagirici <[email protected]>
Signed-off-by: Erhan Cagirici <[email protected]>
Signed-off-by: Erhan Cagirici <[email protected]>
Signed-off-by: Erhan Cagirici <[email protected]>
Signed-off-by: Erhan Cagirici <[email protected]>
Signed-off-by: Erhan Cagirici <[email protected]>
Signed-off-by: Erhan Cagirici <[email protected]>
Signed-off-by: Erhan Cagirici <[email protected]>
656ce55
to
c1eb6c2
Compare
Signed-off-by: Erhan Cagirici <[email protected]>
|
||
/* | ||
This file is forked from upstream kubernetes/client-go | ||
https://github.com/kubernetes/client-go/blob/0b9a7d2f21befcfd98bf2e62ae68ea49d682500d/openapi/groupversion.go |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why?
echo "SSA validation failed! Labels 'some-key' and 'another-key' from both Objects should exist with values 'some-value' and 'another-value' respectively!" | ||
#exit 1 | ||
fi | ||
echo "Successfully validated the SSA feature!" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
is this the only e2e test we have? 😱
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Currently, we rely on the uptest framework for e2e tests just like other providers and what we can do with uptest is limited, but still better than nothing. Also due to some current limitations that is about to be solved, we couldn't really integrate this test into the CI.
I would suggest:
- Merging this PR by manually triggering those tests.
- Fast follow to enable them in CI with the corresponding uptest fix, so that we can make sure they run for every PR.
- Creating a ticket to discuss whether we need a more sophisticated testing framework here.
Signed-off-by: Erhan Cagirici <[email protected]>
My comments are addressed. So as far as I can judge, and under the lack of proper e2e tests, this sgtm. Let's hope there is no firework 💥🍾🤞 |
Description of your changes
This PR adds alpha support for Server Side Apply, renovating the previous attempt.
Fixes #37
Fixes #57
Fixes #114
Fixes #115
Fixes #145
Fixes #269
I have:
make reviewable test
to ensure this PR is ready for review.How has this code been tested
Automated
Manuel
examples/object/object-ssa-owner.yaml
examples/object/object-ssa-labeler.yaml
last-applied-annotation
.Objects
.